# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Security::Ingestion::Tasks::UpdateVulnerabilityUuids, feature_category: :vulnerability_management do
  let(:user) { create(:user) }

  let_it_be(:pipeline) { create(:ci_pipeline) }

  let_it_be(:location1) { create(:ci_reports_security_locations_sast, start_line: 29, end_line: 29) }
  let_it_be(:location2) { create(:ci_reports_security_locations_sast, start_line: 42, end_line: 42) }
  let_it_be(:semgrep_scanner) { build(:ci_reports_security_scanner, external_id: 'semgrep', name: 'Semgrep') }

  let_it_be(:semgrep_id) do
    create(:ci_reports_security_identifier, external_id: 'semgrep', external_type: 'semgrep_id')
  end

  let_it_be(:gosec_id) do
    create(:ci_reports_security_identifier, external_id: 'gosec', external_type: 'type')
  end

  let_it_be(:identifiers) { [semgrep_id, gosec_id] }
  let(:report_finding1) do
    create(:ci_reports_security_finding,
           identifiers: identifiers,
           uuid: original_vulnerability1.uuid,
           location: location1,
           scanner: semgrep_scanner
          )
  end

  let(:report_finding2) do
    create(:ci_reports_security_finding,
           identifiers: identifiers,
           uuid: original_vulnerability2.uuid,
           location: location2,
           scanner: semgrep_scanner
          )
  end

  let(:finding_map1) { create(:finding_map, report_finding: report_finding1) }
  let(:finding_map2) { create(:finding_map, report_finding: report_finding2) }
  let(:service_object) { described_class.new(pipeline, [finding_map1, finding_map2]) }

  let_it_be(:gosec_vuln_id) do
    create(:vulnerabilities_identifier,
           external_type: 'type',
           fingerprint: gosec_id.fingerprint
          )
  end

  let_it_be(:semgrep_vuln_id) do
    create(:vulnerabilities_identifier,
           external_type: 'type',
           fingerprint: semgrep_id.fingerprint
          )
  end

  subject(:update_vulnerability_uuids) { service_object.execute }

  context 'gosec vulnerabilities exist' do
    let(:vulnerability) { create_vulnerability }
    let(:vulnerability2) { create_vulnerability }
    let(:original_vulnerability1) do
      create(:vulnerabilities_finding,
             vulnerability: vulnerability,
             project: pipeline.project,
             primary_identifier: gosec_vuln_id,
             identifiers: [gosec_vuln_id],
             location_fingerprint: location1.fingerprint
            )
    end

    let(:original_vulnerability2) do
      create(:vulnerabilities_finding,
             vulnerability: vulnerability2,
             project: pipeline.project,
             primary_identifier: gosec_vuln_id,
             identifiers: [gosec_vuln_id],
             location_fingerprint: location2.fingerprint
            )
    end

    let!(:feedback1) do
      create(:vulnerability_feedback,
             finding_uuid: original_vulnerability1.uuid,
             project: pipeline.project
            )
    end

    let!(:feedback2) do
      create(:vulnerability_feedback,
             finding_uuid: original_vulnerability2.uuid,
             project: pipeline.project
            )
    end

    it 'updates UUID of existing vulnerabilities' do
      uuids = [original_vulnerability1.uuid, original_vulnerability2.uuid]

      expect( Vulnerabilities::Finding.by_uuid(uuids).count ).to eq(2)
      expect( Vulnerabilities::Feedback.by_finding_uuid(uuids).count ).to eq(2)
      expect( Vulnerabilities::Read.by_uuid(uuids).count ).to eq(2)

      update_vulnerability_uuids
      expect( Vulnerabilities::Finding.by_uuid(uuids).count ).to eq(0)
      expect( Vulnerabilities::Feedback.by_finding_uuid(uuids).count ).to eq(0)
      expect( Vulnerabilities::Read.by_uuid(uuids).count ).to eq(0)
    end

    context 'semgrep vulnerabilities already exist' do
      let(:original_semgrep_vulnerability1) do
        create(:vulnerabilities_finding,
               project: pipeline.project,
               primary_identifier: semgrep_vuln_id,
               identifiers: [semgrep_vuln_id,
                             gosec_vuln_id]
              )
      end

      let(:original_semgrep_vulnerability2) do
        create(:vulnerabilities_finding,
               project: pipeline.project,
               primary_identifier: semgrep_vuln_id,
               identifiers: [semgrep_vuln_id],
               location_fingerprint: location2.fingerprint
              )
      end

      let(:report_finding1a) do
        create(:ci_reports_security_finding,
               identifiers: identifiers,
               uuid: original_semgrep_vulnerability1.uuid,
               location: location1,
               scanner: semgrep_scanner
              )
      end

      let(:report_finding2a) do
        create(:ci_reports_security_finding,
               identifiers: identifiers,
               uuid: original_semgrep_vulnerability2.uuid,
               location: location2,
               scanner: semgrep_scanner
              )
      end

      let(:finding_map1a) { create(:finding_map, report_finding: report_finding1a) }
      let(:finding_map2a) { create(:finding_map, report_finding: report_finding2a) }

      let(:service_objecta) do
        finding_map1a.security_finding.uuid = report_finding1a.uuid
        finding_map2a.security_finding.uuid = report_finding2a.uuid
        described_class.new(pipeline, [finding_map1a, finding_map2a])
      end

      it 'does not raise an exception' do
        gosec_uuids = [original_vulnerability1.uuid, original_vulnerability2.uuid]
        semgrep_uuids = [original_semgrep_vulnerability1.uuid, original_semgrep_vulnerability2.uuid]
        uuids = gosec_uuids + semgrep_uuids

        expect( Vulnerabilities::Finding.where(uuid: uuids).count ).to eq(4)

        expect(Gitlab::ErrorTracking).not_to receive(:track_exception)

        service_objecta.execute
      end
    end
  end

  context 'semgrep vulnerabilities exist' do
    let(:original_vulnerability1) do
      create(:vulnerabilities_finding,
             project: pipeline.project,
             primary_identifier: semgrep_vuln_id,
             identifiers: [semgrep_vuln_id,
                           gosec_vuln_id]
            )
    end

    let(:original_vulnerability2) do
      create(:vulnerabilities_finding,
             project: pipeline.project,
             primary_identifier: semgrep_vuln_id,
             identifiers: [semgrep_vuln_id],
             location_fingerprint: location2.fingerprint
            )
    end

    it 'makes no changes to existing vulnerabilities' do
      original_uuid1 = original_vulnerability1.uuid
      original_uuid2 = original_vulnerability2.uuid
      uuids = [original_uuid1, original_uuid2]

      expect( Vulnerabilities::Finding.where(uuid: uuids).count ).to eq(2)

      update_vulnerability_uuids
      expect( Vulnerabilities::Finding.where(uuid: uuids).count ).to eq(2)
    end
  end

  context 'when updating vulnerability uuids fails' do
    let(:original_vulnerability1) do
      create(:vulnerabilities_finding,
             project: pipeline.project
            )
    end

    let(:original_vulnerability2) do
      create(:vulnerabilities_finding,
             project: pipeline.project
            )
    end

    let(:exception) { RuntimeError.new }

    before do
      allow(Gitlab::ErrorTracking).to receive(:track_exception)
      allow(Security::Ingestion::Tasks::UpdateVulnerabilityUuids).to receive(:update_uuids?).and_return(true)
      allow_next_instance_of(Security::Ingestion::Tasks::UpdateVulnerabilityUuids) do |update|
        allow(update).to receive(:update_uuids).and_raise(exception)
      end
    end

    it 'tracks the exception' do
      update_vulnerability_uuids

      expect(Gitlab::ErrorTracking).to have_received(:track_exception).with(exception, project_id: pipeline.project_id)
    end
  end

  private

  def create_vulnerability(severity: 7, confidence: 7, report_type: 0)
    create(:vulnerability,
           project: pipeline.project,
           author: user,
           severity: severity,
           confidence: confidence,
           report_type: report_type
          )
  end
end
